/*******************************************************************************
 * Copyright (c) 2009 EclipseSource and others. All rights reserved. This
 * program and the accompanying materials are made available under the terms of
 * the Eclipse Public License v1.0 which accompanies this distribution, and is
 * available at http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   EclipseSource - initial API and implementation
 ******************************************************************************/
package org.eclipse.ecf.tutorial.osgi.services.discovery;

import org.eclipse.ecf.internal.osgi.services.discovery.*;

import java.io.Serializable;
import java.net.*;
import java.util.*;
import org.eclipse.ecf.core.identity.*;
import org.eclipse.ecf.core.util.ECFRuntimeException;
import org.eclipse.ecf.core.util.Trace;
import org.eclipse.ecf.discovery.*;
import org.eclipse.ecf.discovery.identity.*;
import org.eclipse.ecf.osgi.services.discovery.ECFServicePublication;
import org.eclipse.ecf.remoteservice.Constants;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.discovery.*;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

/**
 * ServicePublicationHandler bi-directionally connects ECF Discovery API with
 * RFC119 ServicePublication.
 */
public class ServicePublicationHandler implements ServiceTrackerCustomizer, Discovery {

	/**
	 * This map acts as a mapping from ServiceReferences to IServiceInfo objects
	 * and is used to retrieve the IServiceInfo associated with a given
	 * ServiceReference on OSGi service unpublish
	 */
	private Map serviceRefToServiceInfo = Collections.synchronizedMap(new HashMap());

	/*---------------- ECF Discovery --> RFC 119 ----------------*/

	/**
	 * ECF Discovery IServiceListener. Receives IServiceEvents fired by ECF
	 * discovery upon service discovery/un-discovery
	 */
	private final IServiceListener serviceListener = new IServiceListener() {
		/* (non-Javadoc)
		 * @see org.eclipse.ecf.discovery.IServiceListener#serviceDiscovered(org.eclipse.ecf.discovery.IServiceEvent)
		 */
		public void serviceDiscovered(IServiceEvent anEvent) {
			IServiceInfo serviceInfo = anEvent.getServiceInfo();
			IServiceID serviceID = serviceInfo.getServiceID();
			trace("handleOSGIServiceDiscovered", " serviceInfo=" + serviceInfo); //$NON-NLS-1$ //$NON-NLS-2$
			if (checkValid(serviceID)) {
				trace("handleOSGIServiceDiscovered matched", " serviceInfo=" + serviceInfo); //$NON-NLS-1$ //$NON-NLS-2$
				DiscoveredServiceTracker[] discoveredTrackers = findMatchingDiscoveredServiceTrackers(serviceInfo);
				for (int i = 0; i < discoveredTrackers.length; i++) {
					discoveredTrackers[i].serviceChanged(new DiscoveredServiceNotificationImpl(
							DiscoveredServiceNotification.AVAILABLE, serviceInfo));
				}
			}
		}

		/* (non-Javadoc)
		 * @see org.eclipse.ecf.discovery.IServiceListener#serviceUndiscovered(org.eclipse.ecf.discovery.IServiceEvent)
		 */
		public void serviceUndiscovered(IServiceEvent anEvent) {
			IServiceInfo serviceInfo = anEvent.getServiceInfo();
			IServiceID serviceID = serviceInfo.getServiceID();
			if (checkValid(serviceID)) {
				trace("handleOSGIServiceUndiscovered", " serviceInfo=" + serviceInfo); //$NON-NLS-1$ //$NON-NLS-2$
				DiscoveredServiceTracker[] discoveredTrackers = findMatchingDiscoveredServiceTrackers(serviceInfo);
				for (int i = 0; i < discoveredTrackers.length; i++) {
					discoveredTrackers[i].serviceChanged(new DiscoveredServiceNotificationImpl(
							DiscoveredServiceNotification.UNAVAILABLE, serviceInfo));
				}
			}
		}
	};
	

	/**
	 * Creates a new {@link ServicePublicationHandler} and immediately starts listening for ECF discovery events
	 */
	public ServicePublicationHandler() {
		//TODO tutorial "implement listener registration with ECF discovery"
		throw new UnsupportedOperationException("ServicePublicationHandler()");
	}
	
	/*---------------- RFC 119 --> ECF Discovery ----------------*/
	
	/* (non-Javadoc)
	 * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference)
	 */
	public Object addingService(ServiceReference reference) {
		if(checkValid(reference)) {
			publishService(reference);
		}
		return Activator.getDefault().getContext().getService(reference);
	}

	/* (non-Javadoc)
	 * @see org.osgi.util.tracker.ServiceTrackerCustomizer#modifiedService(org.osgi.framework.ServiceReference, java.lang.Object)
	 */
	public void modifiedService(ServiceReference reference, Object service) {
		if(checkValid(reference)) {
			unpublishService(reference);
			publishService(reference);
		}
	}

	/* (non-Javadoc)
	 * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference, java.lang.Object)
	 */
	public void removedService(ServiceReference reference, Object service) {
		if(checkValid(reference)) {
			unpublishService(reference);
		}
	}

	/*----------- Converter methods RFC 119 --> ECF Discovery -----------*/

	/**
	 * Publishes the given ServiceReference with ECF discovery
	 * @param reference The {@link ServiceReference} to be published
	 */
	private void publishService(ServiceReference reference) {
		//TODO tutorial "implement servicePublicationProperties to IServiceType conversion"
		throw new UnsupportedOperationException("publishService(ServiceReference reference)");
	}

	/**
	 * Unpublishes the given ServiceReference
	 * @param reference
	 */
	private void unpublishService(ServiceReference reference) {
		IServiceInfo svcInfo = null;
		try {
			svcInfo = (IServiceInfo) serviceRefToServiceInfo.remove(reference);
			if (svcInfo != null) {
				Activator.getDefault().getAdvertiser().unregisterService(svcInfo);
			}
		} catch (ECFRuntimeException e) {
			logError("publishService", "Cannot unregister serviceInfo=" //$NON-NLS-1$ //$NON-NLS-2$
					+ svcInfo, e);
		}
	}

	/**
	 * 
	 * @param serviceReference
	 * @param serviceInfo
	 * @return
	 */
	private boolean matchWithDiscoveredServiceInfo(ServiceReference serviceReference, IServiceInfo serviceInfo) {
		//TODO tutorial "implement servicePublicationProperties to IServiceType conversion"
		//throw new UnsupportedOperationException("matchWithDiscoveredServiceInfo(ServiceReference serviceReference, IServiceInfo serviceInfo)");
		
		// If short on time, returning true here is fine too
		return true;
	}
	
	/**
	 * Returns an array of {@link DiscoveredServiceTracker}s responsible for the given {@link IServiceInfo} or an empty array if the is none
	 */
	private DiscoveredServiceTracker[] findMatchingDiscoveredServiceTrackers(IServiceInfo serviceInfo) {
		ServiceReference[] sourceTrackers = Activator.getDefault().getDiscoveredServiceTrackerReferences();
		if (sourceTrackers == null) {
			return new DiscoveredServiceTracker[0];
		}
		List matchingTrackers = new ArrayList();
		BundleContext context = Activator.getDefault().getContext();
		for (int i = 0; i < sourceTrackers.length; i++) {
			if (matchWithDiscoveredServiceInfo(sourceTrackers[i], serviceInfo)) {
				matchingTrackers.add(context.getService(sourceTrackers[i]));
			}
		}
		return (DiscoveredServiceTracker[]) matchingTrackers.toArray(new DiscoveredServiceTracker[] {});
	}
	
	/**
	 * Checks the given ServiceReference for validity with this SPH. In order for a {@link ServiceReference}
	 * to be valid, certain properties have to be set:
	 * <p>
	 * 	{@link ServicePublication#PROP_KEY_SERVICE_INTERFACE_NAME\
	 *	{@link ServicePublication#PROP_KEY_SERVICE_PROPERTIES}
	 *  {@link ECFServicePublication#PROP_KEY_ENDPOINT_CONTAINERID}
	 *  {@link Constants#SERVICE_NAMESPACE}
	 *  {@link Constants#SERVICE_ID}
	 * @param reference The reference to be checked
	 * @return true if the given reference conforms to our requirements, false otherwise
	 */
	private boolean checkValid(ServiceReference reference) {
		// ServicePublication.PROP_KEY_SERVICE_INTERFACE_NAME
		Collection svcInterfaces = ServicePropertyUtils.getCollectionProperty(reference,
				ServicePublication.PROP_KEY_SERVICE_INTERFACE_NAME);
		if (svcInterfaces == null) {
			logError("handleServicePublication", //$NON-NLS-1$
					"ignoring " //$NON-NLS-1$
							+ reference + ". ServicePublication.PROP_KEY_SERVICE_INTERFACE_NAME not set", //$NON-NLS-1$
					null);
			return false;
		}

		// We also use the optional RFC 119 property PROP_KEY_SERVICE_PROPERTIES
		Map servicePublicationServiceProperties = ServicePropertyUtils.getMapProperty(reference,
				ServicePublication.PROP_KEY_SERVICE_PROPERTIES);
		if (servicePublicationServiceProperties == null) {
			logError("handleServicePublication", //$NON-NLS-1$
					"ignoring " //$NON-NLS-1$
							+ reference + ". ServicePublication.PROP_KEY_SERVICE_PROPERTIES not set", //$NON-NLS-1$
					null);
			return false;
		}

		// ECFServicePublication.PROP_KEY_ENDPOINT_CONTAINERID
		ID endpointContainerID = (ID) reference.getProperty(ECFServicePublication.PROP_KEY_ENDPOINT_CONTAINERID);
		if (endpointContainerID == null) {
			logError("handleServicePublication", //$NON-NLS-1$
					"ignoring " //$NON-NLS-1$
							+ reference + ". ECFServicePublication.PROP_KEY_ENDPOINT_CONTAINERID not set", //$NON-NLS-1$
					null);
			return false;
		}

		// Constants.SERVICE_NAMESPACE
		String rsnamespace = ServicePropertyUtils.getStringProperty(reference, Constants.SERVICE_NAMESPACE);
		if (rsnamespace == null) {
			logError("handleServicePublication", "ignoring " + reference //$NON-NLS-1$ //$NON-NLS-2$
					+ ". Constants.SERVICE_NAMESPACE not set", null); //$NON-NLS-1$
			return false;
		}

		// Constants.SERVICE_ID
		Long remoteServiceID = (Long) reference.getProperty(Constants.SERVICE_ID);
		if (remoteServiceID == null) {
			logError("handleServicePublication", "ignoring " + reference //$NON-NLS-1$ //$NON-NLS-2$
					+ ". Constants.SERVICE_ID not set", null); //$NON-NLS-1$
			return false;
		}

		return true;
	}

	private void addPropertiesToDiscoveryServiceProperties(IServiceProperties discoveryServiceProperties,
			Map servicePublicationServiceProperties) {
		for (Iterator i = servicePublicationServiceProperties.keySet().iterator(); i.hasNext();) {
			Object key = i.next();
			if (!(key instanceof String)) {
				trace("addPropertiesToDiscoveryServiceProperties", //$NON-NLS-1$
						"skipping non-string key " + key); //$NON-NLS-1$
				continue;
			}
			String keyStr = (String) key;
			Object val = servicePublicationServiceProperties.get(keyStr);
			if (val instanceof String) {
				discoveryServiceProperties.setPropertyString(keyStr, (String) val);
			} else if (val instanceof byte[]) {
				discoveryServiceProperties.setPropertyBytes(keyStr, (byte[]) val);
			} else if (val instanceof Serializable) {
				discoveryServiceProperties.setProperty(keyStr, val);
			}
		}
	}

	/*----------- Converter methods ECF Discovery --> RFC 119 -----------*/
	
	/**
	 * Only accept services of type ECFServicePublication.SERVICE_TYPE
	 */
	private boolean checkValid(IServiceID serviceId) {
		String[] service = serviceId.getServiceTypeID().getServices();
		List asList = Arrays.asList(service);
		if (asList.contains(ECFServicePublication.SERVICE_TYPE)) {
			return true;
		}
		return false;
	}
	
	/**
	 * Creates an {@link URI} from the given {@link ID}
	 * @param endpointContainerID
	 * @return an {@link URI} 
	 * @throws URISyntaxException
	 */
	private URI createURI(ID endpointContainerID) throws URISyntaxException {
		boolean done = false;
		URI uri = null;
		String str = endpointContainerID.getName();
		while (!done) {
			try {
				uri = new URI(str);
				if (!uri.isOpaque()) {
					done = true;
				} else {
					str = uri.getRawSchemeSpecificPart();
				}
			} catch (URISyntaxException e) {
				done = true;
			}
		}
		String scheme = ECFServicePublication.SERVICE_TYPE;
		int port = 32565;
		if (uri != null) {
			port = uri.getPort();
			if (port == -1)
				port = 32565;
		}
		String host = null;
		try {
			host = InetAddress.getLocalHost().getHostAddress();
		} catch (Exception e) {
			host = "localhost"; //$NON-NLS-1$
		}
		return new URI(scheme, null, host, port, null, null, null);
	}

	/**
	 * Creates an {@link IServiceTypeID} from the given servicePublicationProperties
	 * @param servicePublicationProperties
	 * @param aNamespace
	 * @return
	 * @throws IDCreateException
	 */
	private IServiceTypeID createServiceTypeID(Map servicePublicationProperties, Namespace aNamespace) throws IDCreateException {
		//TODO tutorial "implement servicePublicationProperties to IServiceType conversion"
		throw new UnsupportedOperationException("createServiceTypeID(Map servicePublicationProperties, Namespace aNamespace)");
	}
	
	/*----------- Utility methods -----------*/
	
	/**
	 * Returns the object associated with the given key from the map or default if map does not contains key
	 */
	private String getPropertyWithDefault(Map properties, String key, String def) {
		String val = (String) properties.get(key);
		return (val == null) ? def : val;
	}

	/**
	 * Log an Error with the OSGi LogService. Additionally sends a trace.
	 */
	private void logError(String method, String message, Throwable t) {
		Activator.getDefault().log(method, message, t);
	}

	/**
	 * Trace output
	 */
	private void trace(String methodName, String message) {
		Trace.trace(Activator.PLUGIN_ID, DebugOptions.SVCPUBHANDLERDEBUG, this.getClass(), methodName, message);
	}

	/**
	 * Cleans up after bundle life-cycle stop event. Most importantly it unregisters all registered services with ECF discovery
	 */
	public void dispose() {
		IDiscoveryLocator locator = Activator.getDefault().getLocator();
		if (locator != null) {
			locator.removeServiceListener(serviceListener);
			synchronized (serviceRefToServiceInfo) {
				for (Iterator i = serviceRefToServiceInfo.keySet().iterator(); i.hasNext();) {
					ServiceReference sr = (ServiceReference) i.next();
					unpublishService(sr);
				}
				serviceRefToServiceInfo.clear();
			}
			locator = null;
		}
	}
}
